package uk.co.cdal.cobweb.backend;

import java.io.IOException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.logging.FileHandler;
import java.util.logging.Logger;

import javax.swing.ImageIcon;

import uk.co.cdal.cobweb.frontend.Frontend;
import uk.co.cdal.cobweb.shared.Idea;

public class BackendImpl extends UnicastRemoteObject implements Backend {

	/**
	 * Instance varibles
	 */
	private static final long serialVersionUID = -7419435486563774174L;

	private Frontend frontend;

	private Date date;

	private Backend parent;

	private Backend child0;

	private Backend child1;
	
	private Backend uncle;
	
	private Backend grandparent;
	
	private Backend sibling;

	private String username;

	private int requestsPassed;
	
	private int port;
	
	private boolean connected;
	
	private HashMap<String, Idea> ideas;
	
	private int nodeCount;
	
	public static final Logger log = Logger.getLogger(BackendImpl.class.getName());
	
	
	public BackendImpl() throws RemoteException {
		super(8000);
		port = 8000;
		init();
	}
	
	public void bind() {
		try {
			String hostname = InetAddress.getLocalHost().getHostAddress();
			Naming.rebind("rmi://" + hostname + "/" + Backend.rmiName, this);
			System.out.println("Bound to " + hostname);
			setConnected(true);
		} 
		catch (MalformedURLException e) {
			e.printStackTrace();
			log.config("The specified URL was invalid");
		} 
		catch (UnknownHostException e) {
			e.printStackTrace();
			log.config("Unknown host encountered");
		}
		catch(RemoteException e) {
			e.printStackTrace();
			log.config("Remote exception encoutered");
		}
	}
	
	public void bind(String hostname) {
		
		try {
			if(hostname.equals(InetAddress.getLocalHost().getHostAddress())) {
				throw new MalformedURLException();
			}
			else {
				String name = "rmi://" + hostname + "/" + Backend.rmiName;
				hostname = InetAddress.getLocalHost().getHostAddress();
				Naming.rebind("rmi://" + hostname + "/" + Backend.rmiName, this);
				parent = (Backend) Naming.lookup(name);
				parent.connect(InetAddress.getLocalHost());
				setConnected(true);
			}
		} 
		catch (MalformedURLException e) {
			log.config("The URL is malformed or you are trying to connect to yourself");
		} 
		catch (NotBoundException e) {
			e.printStackTrace();
			log.config("The registry is not bound");
		} 
		catch (UnknownHostException e) {
			e.printStackTrace();
			log.config("The host is unknown");
		}
		catch (RemoteException e) {
			log.config("Remote exception occured");
		}
	}
	
	private  void init() {
		date = new Date();
		ideas = new HashMap<String, Idea>();
		username = "Default User";
		requestsPassed = 0;
		nodeCount = 0;
		frontend = Frontend.getFrontend(this);
		frontend.init();
		setConnected(false);
		try {
			log.addHandler(new FileHandler("cobweb.log.%g", true));
		}
		catch (IOException e){
			e.printStackTrace();
			System.out.println("Error cannot write to logs.");
		}
	}

	public void sendMessage(String from, String message, String date)
			throws RemoteException {
		log.info(from + " says: " + message);
		frontend.addChatItem(from, message, date);
	}

	/**
	 * Create new message on local frontend then send to remote backends.
	 * 
	 * @param message
	 */
	public void newMessage(String message) {
		try {
			log("New message recieved: " + message);
			sendMessage(username, message, date.toString());
			String date = this.date.toString();
			if (!(parent == null)) {
				parent.sendMessage(username, message, date);
			}
			if (!(child0 == null)) {
				child0.sendMessage(username, message, date);
			}
			if (!(child1 == null)) {
				child1.sendMessage(username, message, date);
			}
		} 
		catch (RemoteException e) {
			log("Unable to contact peer");
			checkPeers();
		}

	}
	
	/**
	 * Add the given idea to the collection of ideas
	 * 
	 * @param i the idea to be added
	 */
	public void addIdea(Idea i, InetAddress addr) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		ideas.put(i.getId(), i);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(addr != parent.getAddress()) {
				parent.addIdea(i, getAddress());
			}
		}
		if(child0 != null) {
			if(addr != child0.getAddress()) {
				child0.addIdea(i, getAddress());
			}
		}
		if(child1 != null) {
			if(addr != child1.getAddress()) {
				child1.addIdea(i, getAddress());
			}
		}
	}
	
	/**
	 * Remove the specified idea from the collection.
	 * 
	 * @param i the Idea to be removed.
	 */
	public void removeIdea(Idea i, InetAddress addr) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		if(i.getParent()!= null) {
			i.getParent().removeChild(i);
		}
		if(i.hasChildren()) {
			for(int j= 0; j < i.getChildren().size(); j++) {
				i.getChildren().get(j).setParent(null);
			}
		}
		ideas.remove(i);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(addr != parent.getAddress()) {
				parent.removeIdea(i, getAddress());
			}
		}
		if(child0 != null) {
			if(addr != child0.getAddress()) {
				child0.removeIdea(i, getAddress());
			}
		}
		if(child1 != null) {
			if(addr != child1.getAddress()) {
				child1.removeIdea(i, getAddress());
			}
		}
	}

	/**
	 * returns the InetAddress of the local node. For use in determining where
	 * to send messages onto later.
	 * 
	 * @return {@link InetAddress}
	 * @return null if {@link UnknownHostException} is thrown.
	 */
	public InetAddress getAddress() throws RemoteException {
		try {
			return InetAddress.getLocalHost();
		} 
		catch (UnknownHostException e) {

		}
		return null;
	}

	public boolean stillThere() throws RemoteException {
		return true;
	}


	public long getAvgConnectionTime(Backend b) throws RemoteException {
		long start = System.currentTimeMillis();
		b.stillThere();
		long run1 = System.currentTimeMillis() - start;
		start = System.currentTimeMillis();
		b.stillThere();
		long run2 = System.currentTimeMillis() - start;
		start = System.currentTimeMillis();
		b.stillThere();
		long run3 = System.currentTimeMillis() - start;
		return (run1+run2+run3)/3;
	}
	
	public void connect(InetAddress addr) throws RemoteException {
		String name = "//" + addr.getHostName() + "/" + BackendImpl.rmiName;
		try {
			/*
			 * To make network adaptive and self optimising make children
			 * connect and compare using some network metric. Don't forget to
			 * compare new node to parent. Once comparisons made reassign
			 * child/parent in some sensible fashion.
			 */
			Backend temp = (Backend) Naming.lookup(name);
			long parTime = 100000000000L;
			long localTime;
			long ch0Time = 100000000000L;
			long ch1Time = 100000000000L;
			long bias = 40;
			
			if(parent != null) {
				parTime = parent.getAvgConnectionTime(temp);
			}
			localTime = getAvgConnectionTime(temp);
			if(child0 != null) {
				ch0Time = child0.getAvgConnectionTime(temp);
			}
			if(child1 != null) {
				ch1Time = child1.getAvgConnectionTime(temp);
			}
			
			if(localTime + bias < parTime) {
				parent.setMe(temp, getAddress());
				parent = temp;
			}
			//TODO: check children times compared to local.
			
			//TODO: Add code to make network adaptive
			if (child0 == null) {
				child0 = (Backend) Naming.lookup(name);
			} else if (child1 == null) {
				child1 = (Backend) Naming.lookup(name);
			} else {
				if ((requestsPassed % 2) == 0) {
					child0.connect(addr);
					requestsPassed++;
				} else {
					child1.connect(addr);
					requestsPassed++;
				}
			}
		} 
		catch (MalformedURLException e) {
		
		} 
		catch (NotBoundException e) {
		
		}
	}
	
	public void addImage(ImageIcon image, Idea i, InetAddress from) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		ideas.get(i.getId()).addImage(image);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(from != parent.getAddress()) {
				parent.addImage(image, i, getAddress());
			}
		}
		if(child0 != null) {
			if(from != child0.getAddress()) {
				child0.addImage(image, i, getAddress());
			}
		}
		if(child1 != null) {
			if(from != child1.getAddress()) {
				child1.addImage(image, i, getAddress());
			}
		}
	}

	public void addURL(String URL, Idea i, InetAddress from) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		ideas.get(i.getId()).addURL(URL);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(from != parent.getAddress()) {
				parent.addURL(URL, i, getAddress());
			}
		}
		if(child0 != null) {
			if(from != child0.getAddress()) {
				child0.addURL(URL, i, getAddress());
			}
		}
		if(child1 != null) {
			if(from != child1.getAddress()) {
				child1.addURL(URL, i, getAddress());
			}
		}
	}

	public void removeImage(ImageIcon image, Idea i, InetAddress from) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		ideas.get(i.getId()).removeImage(image);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(from != parent.getAddress()) {
				parent.removeImage(image, i, getAddress());
			}
		}
		if(child0 != null) {
			if(from != child0.getAddress()) {
				child0.removeImage(image, i, getAddress());
			}
		}
		if(child1 != null) {
			if(from != child1.getAddress()) {
				child1.removeImage(image, i, getAddress());
			}
		}
	}

	public void removeURL(String URL, Idea i, InetAddress from) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		ideas.get(i.getId()).removeURL(URL);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(from != parent.getAddress()) {
				parent.removeURL(URL, i, getAddress());
			}
		}
		if(child0 != null) {
			if(from != child0.getAddress()) {
				child0.removeURL(URL, i, getAddress());
			}
		}
		if(child1 != null) {
			if(from != child1.getAddress()) {
				child1.removeURL(URL, i, getAddress());
			}
		}
	}

	public void updateFurtherText(String futherText, Idea i, InetAddress addr) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		ideas.get(i.getId()).setFurtherText(futherText);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(addr != parent.getAddress()) {
				parent.updateFurtherText(futherText, i, getAddress());
			}
		}
		if(child0 != null) {
			if(addr != child0.getAddress()) {
				child0.updateFurtherText(futherText, i, getAddress());
			}
		}
		if(child1 != null) {
			if(addr != child1.getAddress()) {
				child1.updateFurtherText(futherText, i, getAddress());
			}
		}
	}

	public void updateTitle(String title, Idea i, InetAddress addr) throws RemoteException {
		// Add the idea locally then send to all connected except from where it came from
		// Avoiding network loops
		ideas.get(i.getId()).setTitle(title);
		frontend.update();
		
		// Send to all connected relations
		if(parent != null) {
			if(addr != parent.getAddress()) {
				parent.updateTitle(title, i, getAddress());
			}
		}
		if(child0 != null) {
			if(addr != child0.getAddress()) {
				child0.updateTitle(title, i, getAddress());
			}
		}
		if(child1 != null) {
			if(addr != child1.getAddress()) {
				child1.updateTitle(title, i, getAddress());
			}
		}
	}
	
	private void checkPeers() {
		checkParent();
		checkChild0();
		checkChild1();
	}
	
	private void checkParent() {
		try {
			parent.stillThere();
		}
		catch(RemoteException e) {
			parent = null;
			log.info("Parent node cannot be contacted finding a new parent.");
			findNewParent();
		}
	}
	
	private void checkChild0() {
		try{
			child0.stillThere();
		}
		catch(RemoteException e) {
			child0 = null;
			log.info("Child 0 could not be contacted.");
		}
	}
	
	private void checkChild1() {
		try {
			child1.stillThere();
		}
		catch(RemoteException e) {
			child1 = null;
			log.info("Child 1 could not be contacted.");
		}
	}
	
	private void findNewParent() {
		if(grandparent != null) {
			try{
				grandparent.connect(getAddress());
			}
			catch (RemoteException e) {
				log.severe("Could not resolve localhost");
			}
		}
		else if(uncle != null) {
			try{
				uncle.connect(getAddress());
			}
			catch (RemoteException e) {
				log.severe("Could not resolve localhost");
			}
		}
		else if(sibling != null) {
			// Last resort - add sibling as parent
			try{
				sibling.connect(getAddress());
			}
			catch (RemoteException e) {
				log.severe("Could not resolve localhost");
			}
		}
		// Update family relations
		try {
			grandparent = parent.getParent();
			if(parent == null) {
				// You are the top of the tree
				sibling = null;
			}
			else if(parent.getChild0().getAddress() == getAddress()) {
				sibling = parent.getChild1();
			}
			else {
				sibling = parent.getChild0();
			}
			
			if(parent != null) {
				uncle = parent.getSibling();
			}
			else{
				uncle = null;
			}
			
		}
		catch(RemoteException e) {
			log.info("An error occured when locating a new parent checking peers");
			checkPeers();
		}
		finally {
			// Parent should ne set to something by now if not then prob singleuser mode
			if(parent != null) {
				try {
					ArrayList<String> ideas = parent.panic();
					
				}
				catch(RemoteException e) {
					checkPeers();
					log.info("Panic Mode failed");
				}
				finally {
					
				}
			}
			// No connection to other peers including children.
			else if (child0 == null && child1 == null) {
				unbind();
			}
		}
	}
	
	public void unbind() {
		try {
			String hostname = InetAddress.getLocalHost().getHostAddress();
			Naming.unbind("rmi://" + hostname + "/" + Backend.rmiName);
		}
		catch(NotBoundException e) {
			log.info("I'm already unbound");
		}
		catch(RemoteException e) {
			log.info("Couldn't unbind from the local registry");
		}
		catch(UnknownHostException e) {
			log.info("The hostname given could not be resolved.");
		}
		catch(MalformedURLException e) {
			log.info("The URL was not specified in the correct syntax.");
		}
	}
	

	public void setMe(Backend inPlace, InetAddress addr) throws RemoteException {
		if(child0 != null) {
			if(child0.getAddress() == addr) {
				child0 = inPlace;
			}
		}
		
		if(child1 != null) {
			if(child1.getAddress() == addr) {
				child1 = inPlace;
			}
		}
	}
	
	public ArrayList<String> panic() throws RemoteException {
		// TODO Auto-generated method stub
		return null;
	}

	public Idea requestIdea(String ideaId) throws RemoteException {
		// TODO Auto-generated method stub
		return null;
	}

	public void setUsername(String username) {
		this.username = username;
	}
	
	public int getPort() {
		return port;
	}
	
	public static void log(String message) {
			log.config(message);
	}
	
	public int getNodeCount() {
		nodeCount++;
		return nodeCount - 1;
	}
	
	/**
	 * A method for obtaining all the ideas (used in the drawing process)
	 * @return all current ideas (including nulls if ideas jave been removed)
	 */
	public ArrayList<Idea> getIdeas() {
		return new ArrayList<Idea>(this.ideas.values());
	}

	/**
	 * @return the child0
	 */
	public Backend getChild0() {
		return child0;
	}

	/**
	 * @param child0 the child0 to set
	 */
	public void setChild0(Backend child0) {
		this.child0 = child0;
	}

	/**
	 * @return the child1
	 */
	public Backend getChild1() {
		return child1;
	}

	/**
	 * @param child1 the child1 to set
	 */
	public void setChild1(Backend child1) {
		this.child1 = child1;
	}

	/**
	 * @return the grandparent
	 */
	public Backend getGrandparent() {
		return grandparent;
	}

	/**
	 * @param grandparent the grandparent to set
	 */
	public void setGrandparent(Backend grandparent) {
		this.grandparent = grandparent;
	}

	/**
	 * @return the parent
	 */
	public Backend getParent() {
		return parent;
	}

	/**
	 * @param parent the parent to set
	 */
	public void setParent(Backend parent) {
		this.parent = parent;
	}

	/**
	 * @return the sibling
	 */
	public Backend getSibling() {
		return sibling;
	}

	/**
	 * @param sibling the sibling to set
	 */
	public void setSibling(Backend sibling) {
		this.sibling = sibling;
	}

	/**
	 * @return the uncle
	 */
	public Backend getUncle() {
		return uncle;
	}

	/**
	 * @param uncle the uncle to set
	 */
	public void setUncle(Backend uncle) {
		this.uncle = uncle;
	}

	/**
	 * @return the username
	 */
	public String getUsername() {
		return username;
	}
	

	/**
	 * @return the connected
	 */
	public boolean isConnected() {
		return connected;
	}

	/**
	 * @param connected the connected to set
	 */
	public void setConnected(boolean connected) {
		this.connected = connected;
	}
	
	public static void main(String[] args) {
		BackendImpl backend;
		if (System.getSecurityManager() == null) {
			System.setSecurityManager(new RMISecurityManager());
		}
		try {

			if (args.length == 0) {
				backend = new BackendImpl();
			}
			
//			DrawingPanel dp = Frontend.getFrontend().getDrawingPanel();
//			Random rng = new Random();
//			for (int i = 0; i < 10; i++) {
//				dp.addIdea(rng.nextInt(400), rng.nextInt(300));
//			}
//			
//			int cMax = dp.getComponentCount(); 
//			for (int c = 0; c < cMax; c++) {
//				System.out.println("Component: " + dp.getComponent(c));
//			}
			
			
		} catch (RemoteException e) {
			e.printStackTrace();
		}
		finally {
		}
	}
}
